/*
 * Copyright 2014 Щекотова Ангелина Александровна.
 * Этот файл — часть J4JClientServer Framework
 * J4JClientServer Framework - свободная программа: вы можете перераспространять ее и/или изменять
 * ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде, в каком
 * она была опубликована Фондом свободного программного обеспечения; либо
 * версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.
 *
 * J4JClientServer Framework распространяется в надежде, что она будет полезной,
 * но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
 * или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
 * общественной лицензии GNU.
 *
 * Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
 * вместе с этой программой. Если это не так, см.
 * <http://www.gnu.org/licenses/>.
 */

package ru.java4java.ClientServerFramework;

import ru.java4java.ClientServerFramework.interfaces.*;
import ru.java4java.utils.*;
import org.apache.log4j.Logger;

import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

/** <pre>
 * Класс сервера.
 *
 * Этот класс используется для автоматического приема входящих подключений и отправки их в отдельный поток обработчику клиентов.
 * Запускайте этот класс при помощи {@link java.lang.Thread} класса.
 *
 * Дли настройки сервера вам могут понадобиться следующие методы:
 * {@link Server#Server(int)}
 * {@link ru.java4java.ClientServerFramework.Server#getSocket()}
 * {@link Server#addServerListener(ru.java4java.ClientServerFramework.interfaces.ServerListener)}
 * {@link Server#setClientOptionsClass(Class)}
 * {@link Server#setDefaultSocketHandlerOptions(SocketHandlerOptions)}
 * {@link Server#setDefaultClientTimeout(int)}
 *
 * </pre>
 */
public class Server implements Runnable, Closeable {

    private static final Logger log = Logger.getLogger(Server.class);

    private static int serverNumber = 1;

    private ServerSocket serverSocket;

    private Thread createdThread;
    private Thread runThread;

    private boolean acceptNewConnections = true;

    private final Vector<ServerListener> serverListeners = new Vector<>();

    private Socket clientSocket;

    private Class<? extends SocketHandler> clientHandlerClass = null;
    private final Vector<SocketHandler> socketHandlers = new Vector<>();

    private Class<? extends SocketHandlerOptions> clientOptionsClass = null;



    private SocketHandlerOptions defaultSocketHandlerOptions = null;

    private int defaultClientTimeout = 30000;

    public SocketHandlerOptions getDefaultSocketHandlerOptions() { return defaultSocketHandlerOptions; }

    public void setDefaultSocketHandlerOptions(SocketHandlerOptions defaultSocketHandlerOptions) { this.defaultSocketHandlerOptions = defaultSocketHandlerOptions; }

    public void setClientOptionsClass(Class<? extends SocketHandlerOptions> clazz) { clientOptionsClass = clazz; }

    public Class<? extends SocketHandlerOptions> getClientOptionsClass() {
        if (clientOptionsClass == null) {
            return SocketHandlerOptions.class;
        } else {
            return clientOptionsClass;
        }
    }

    public SocketHandlerOptions getClientOptionInstance() {
        if (defaultSocketHandlerOptions != null) {
            return defaultSocketHandlerOptions;
        } else try {
            return getClientOptionsClass().newInstance();
        } catch (InstantiationException e) {
            log.fatal("InstantiationException", e);
            throw new RuntimeException("InstantiationException", e);
        } catch (IllegalAccessException e) {
            log.fatal("IllegalAccessException", e);
            throw new RuntimeException("IllegalAccessException", e);
        }
    }

    /**
     * Создает сервер, связанный с указанным портом.
     *
     * @param port номер порта, или <code>0</code> для определения порта автоматически.
     *
     * @exception  IOException {@link java.net.ServerSocket#ServerSocket(int)}
     * @exception  SecurityException {@link java.net.ServerSocket#ServerSocket(int)}
     * @exception  IllegalArgumentException {@link java.net.ServerSocket#ServerSocket(int)}
     * @see ru.java4java.ClientServerFramework.Server#getSocket() getSocket() method
     * @see java.net.ServerSocket ServerSocket class
     * @see java.net.ServerSocket#ServerSocket(int) ServerSocket constructor
     */
    public Server(int port) throws IOException {
        serverSocket = new ServerSocket(port);
        createdThread = Thread.currentThread();
    }

    /**
     * Этот таймаут будет установлен обработчику клиента при создании нового подключения.
     * @param clientTimeout В миллисекундах
     * @see Socket#setSoTimeout(int)
     */
    public void setDefaultClientTimeout(int clientTimeout) { this.defaultClientTimeout = clientTimeout; }

    /**
     * @return defaultClientTimeout в миллисекундах
     */
    public int getDefaultClientTimeout() { return defaultClientTimeout; }

    /**<pre>
     * Возвратит все обработчики, которые запущены при помощи данного сервера.
     * </pre>
     * @return List-SocketHandler-s
     */
    public List<SocketHandler> getAllClientHandlersOnThisServer() {
        List<SocketHandler> result = new ArrayList<>();
        cleanClientsList();
        for (SocketHandler handler: socketHandlers) {
            result.add(handler);
        }
        return result;
    }

    /**
     * @see java.lang.Thread
     * @see java.lang.Thread#Thread(Runnable target)
     * @see java.lang.Thread#Thread(Runnable target, String name)
     * @see java.lang.Thread#Thread(ThreadGroup group, Runnable target)
     * @see java.lang.Thread#Thread(ThreadGroup group, Runnable target, String name)
     * @see java.lang.Thread#Thread(ThreadGroup group, Runnable target, String name, long stackSize)
     * @deprecated use {@link java.lang.Thread} class and {@link Thread#start()} method for run this object.
     */
    @Override
    @Deprecated // Not is Deprecated
    public void run() {
        prepareServer();
        log.info("Server is started " + serverSocket.getLocalPort());
        try {
            ServerListenerAdapter.runListeners(this, serverListeners, ServerListenerAdapter.OperationType.BEFORE_START);
            while (!serverSocket.isClosed()) {
                cleanClientsList();
                clientSocket = null;
                try {
                    ServerListenerAdapter.runListeners(this, serverListeners, ServerListenerAdapter.OperationType.BEFORE_ACCEPT);
                    log.info("Wait new connections...");
                    clientSocket = serverSocket.accept(); // Wait new client(accept)
                    ServerListenerAdapter.runListeners(this, serverListeners, ServerListenerAdapter.OperationType.AFTER_ACCEPT);
                    if (acceptNewConnections) {
                        newConnection(clientSocket);      // Handle client
                    }
                } catch (IOException e) {
                    String msg = SocketUtils.isAvailable(serverSocket);
                    if (msg.isEmpty()) {
                        log.error("IOException", e);
                    } else {
                        log.info(msg + ". ServerSocket" + serverSocket);
                        shutdown();
                    }
                }
            }
        } finally {
            shutdown();
        }
    }

    public void addServerListener(ServerListener listener) { serverListeners.add(listener); }

    public void removeServerListener(int index) { serverListeners.remove(index); }

    public int getServerListenerCount() { return serverListeners.size(); }

    public void setClientSocket(Socket socket) { clientSocket = socket; }

    public Socket getClientSocket(Socket socket) { return clientSocket; }

    public void setAcceptNewConnections(boolean flag) { acceptNewConnections = flag; }

    public boolean isAcceptConnections() { return acceptNewConnections; }

    /**
     * @return {@link java.net.ServerSocket} текущего сервера
     * @see java.net.ServerSocket
     */
    public ServerSocket getSocket() { return serverSocket; }

    @Deprecated // Not is Deprecated :)
    public void changeSocket(ServerSocket serverSocket) { this.serverSocket = serverSocket; }

    /**<pre>
     * Установит класс, который будет использоваться для создания нового объекта обработчика.
     * Если вы указываете свой класс обработчика, позаботьтесь о конструкторе:
     * {@link SocketHandler#SocketHandler(java.net.Socket)}
     * </pre>
     * @param clazz Class(? extends SocketHandler)
     */
    public void setClientHandlerClass(Class<? extends SocketHandler> clazz) {
        this.clientHandlerClass = clazz;
    }

    /**
     * Returns the class that will be used to create SocketHandler
     * @return Class(? extends SocketHandler)
     */
    public Class<? extends SocketHandler> getClientHandlerClass() {
        if (clientHandlerClass != null) {
            return clientHandlerClass;
        } else {
            return SocketHandler.class;
        }
    }

    /**<pre>
     * <b>Используйте это -> {@link ru.java4java.ClientServerFramework.Server#shutdown()}</b>
     * </pre>
     * @see ru.java4java.ClientServerFramework.Server#shutdown()
     */
    @Override
    @Deprecated // Not is Deprecated :)
    public void close() {
        if (!serverSocket.isClosed()) {
            ClosingUtils.close(serverSocket);
        }
    }

    /**<pre>
     * Завершение работы всех клиентов на этом сервере, а сам сервер.
     * </pre>
     * @see java.net.Socket#close()
     * @see java.net.ServerSocket#close()
     */
    @SuppressWarnings("deprecation")
    public void shutdown() {
        if (!shutdown) {
            ServerListenerAdapter.runListeners(this, serverListeners, ServerListenerAdapter.OperationType.BEFORE_SHUTDOWN);
            log.info("Shutdown all clients...");
            shutdownAllClients();
            log.info("All Clients are closed.");
            log.info("Shutdown server on port " + serverSocket.getLocalPort() + "...");
            close();
            log.info("Server is closed on port " + serverSocket.getLocalPort() + ".");
            ServerListenerAdapter.runListeners(this, serverListeners, ServerListenerAdapter.OperationType.AFTER_SHUTDOWN);
        }
        shutdown = true;
    }

    private boolean shutdown = false;

    /**
     * Завершение всех клиентов на этом сервере, но сервер продолжает работать
     * @see java.net.Socket#close()
     */
    public void shutdownAllClients() {
        ServerListenerAdapter.runListeners(this, serverListeners, ServerListenerAdapter.OperationType.BEFORE_SHUTDOWN_CLIENTS);
        for (int i = socketHandlers.size() - 1; i >= 0; i--) {
            SocketHandler handler  = socketHandlers.get(i);
            if (!handler.isClosed()) {
                handler.shutdown();
            }
        }
        cleanClientsList();
        ServerListenerAdapter.runListeners(this, serverListeners, ServerListenerAdapter.OperationType.AFTER_SHUTDOWN_CLIENTS);
    }

    private void cleanClientsList() {
        for (int i = socketHandlers.size() - 1; i >= 0; i--) {
            SocketHandler handler = socketHandlers.get(i);
            if (handler.isClosed()) {
                socketHandlers.remove(i);
            }
        }
    }

    private void prepareServer() {
        validateAndPrepareThread();
    }

    private void validateAndPrepareThread() {
        if (Thread.currentThread() == createdThread) {
            throw new ServerRunException("Use Thread class for run Server Class as Runnable class.");
        }
        if (runThread != null) {
            throw new ServerRunException("The Server was already running.");
        }
        runThread = Thread.currentThread();
        runThread.setName("Server-Thread-" + (serverNumber++));
    }

    private void newConnection(Socket socket) throws IOException {
        log.info("New connection. " + socket.toString());

        try {
            socket.setSoTimeout(defaultClientTimeout);
        } catch (IOException e) {
            log.error("Set Socket Timeout. Socket " + socket);
            throw e;
        }

        log.info("Create Client Handler...");
        SocketHandler handler = newClientHandler(socket);
        setOptions(handler);
        registerClientHandler(handler);
        startClientHandler(handler);
        log.info("Client handler was started.");
    }

    private void setOptions(SocketHandler handler) {
        handler.setOptions(getClientOptionInstance());
    }

    private void registerClientHandler(SocketHandler handler) {
        socketHandlers.add(handler);
    }

    private void startClientHandler(SocketHandler handler) {
        Thread thread = new Thread(handler);
        thread.start();
    }

    private SocketHandler newClientHandler(Socket socket) {
        Class<? extends SocketHandler> clazz = getClientHandlerClass();
        try {
            Constructor<? extends SocketHandler> constructor = clazz.getConstructor(Socket.class);
            constructor.setAccessible(true);
            return constructor.newInstance(socket); // Create new SocketHandler
        }
        catch (
          NoSuchMethodException |
            InvocationTargetException |
            InstantiationException |
            IllegalAccessException e)
        {
            log.fatal(e.getClass().getSimpleName(), e);
            shutdown();
            throw new RuntimeException(e.getClass().getSimpleName(), e);
        }
    }

}
